import {
    hasProperty,
    stringIsInteger,
    stringToFloat,
    stringToInteger
} from "../../common/util";
import {
    MDMCategoryElementTypeFlags,
    MDMFieldFlags,
    MDMLabelTypeFlags,
    MDMSortOrder,
    MDMUsageTypeFlags,
    MDMUserContextFlags
} from "../analysis/types";
import {
    Alignments,
    BorderStyles,
    Class,
    Compound,
    ControlTypes,
    CursorTypes,
    DataTypes,
    DisplayOrders,
    ElementAlignments,
    ElementTypes,
    ImagePositions,
    IMDMObject,
    InfoTypes,
    MDMDocument,
    MDMElement,
    MDMElementInstance,
    MDMElements,
    MDMElementTypeName,
    MDMField,
    MDMListOf,
    ObjectFlags,
    Orientations,
    Page,
    Variable,
    VariableUsageFlags,
    VerticalAlignments
} from "./types";

export function bitAnd(arg1: number, arg2: number): boolean {
    return (arg1 & arg2) > 0;
}

export function bitOr(arg1: number, arg2: number): boolean {
    return (arg1 | arg2) > 0;
}

export function getUuid(): string {
    let timestamp = new Date().getTime();
    let perforNow = (typeof performance !== 'undefined' && performance.now() && performance.now()) || 0;
    return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, c => {
        let random = Math.random() * 16;
        if (timestamp > 0) {
            random = (timestamp + random) % 16 | 0;
            timestamp = Math.floor(timestamp / 16);
        }
        else {
            random = (perforNow + random) % 16 | 0;
            perforNow = Math.floor(perforNow / 16);
        }
        return (c === 'x' ? random : (random & 0x3) | 0x8).toString(16);
    });
}

export function correctUuid(uuid: string): string {
    return uuid.length > 0 && uuid.startsWith("_") ? uuid.substring(1) : uuid;
}

export function formateValueByDataType(value: string, dataType: DataTypes): string {
    return dataType === DataTypes.text || dataType === DataTypes.date ? `"${value}"` : value;
}

export function getFactorType(factor: number | boolean | string): string {
    return typeof factor === "number" ? "3" : typeof factor === "string" ? "8" : "11";
}

export function insertToArray<T>(array: T[], element: T, index: number) {
    if (index >= 0 && index < array.length) {
        const shiftItems: T[] = array.slice(index);
        array[index] = element;
        for (let i = 0; i < shiftItems.length; i++) {
            const e = shiftItems[i];
            if (i === shiftItems.length - 1) {
                array.push(e);
            }
            else {
                array[index + i] = e;
            }
        }
    }
    else {
        array.push(element);
    }
}

export function joinString(...str: (string | undefined)[]): string {
    return str.filter(e => e !== undefined).join(",");
}

export function isScriptElementType(type: ElementTypes): boolean {
    return (type >= ElementTypes.analysisSubheading && type <= ElementTypes.analysisMaximum) ||
            type === ElementTypes.analysisCategory;
}

export function booleanToString(value: boolean) {
    return value ? "-1" : "0";
}

export function stringToBoolean(value?: string) {
    return value === undefined ? undefined :
        value === "-1" ? true :
        value.toLowerCase() === "true" ? true :
        value.toLowerCase() === "false" ? false :
        stringIsInteger(value) ? false : undefined;
}

export function getMDMObjectEntries<T extends IMDMObject>(array?: readonly T[]): [string, T][] | undefined {
    if (!array) {
        return undefined;
    }
    let entries: [string, T][] = [];
    for (const item of array) {
        entries.push([item.uuid, item]);
    }
    return entries;
}

// enums

export function stringToControlType(str?: string): ControlTypes | undefined {
    switch (str?.toLowerCase()) {
        case "static":         return ControlTypes.static;
        case "edit":           return ControlTypes.edit;
        case "singlelineedit": return ControlTypes.singleLineEdit;
        case "multilineedit":  return ControlTypes.multiLineEdit;
        case "droplist":       return ControlTypes.dropList;
        case "combolist":      return ControlTypes.comboList;
        case "radiobutton":    return ControlTypes.radioButton;
        case "checkbutton":    return ControlTypes.checkButton;
        case "listbox":        return ControlTypes.listBox;
        case "listcontrol":    return ControlTypes.listControl;
        case "button":         return ControlTypes.button;
        case "date":           return ControlTypes.date;
        case "time":           return ControlTypes.time;
        case "datetime":       return ControlTypes.dateTime;
        case "password":       return ControlTypes.password;
    }
}

export function controlTypeToString(type?: ControlTypes): string | undefined {
    switch (type) {
        case ControlTypes.static:          return "Static";
        case ControlTypes.edit:            return "Edit";
        case ControlTypes.singleLineEdit:  return "SingleLineEdit";
        case ControlTypes.multiLineEdit:   return "MultiLineEdit";
        case ControlTypes.dropList:        return "DropList";
        case ControlTypes.comboList:       return "ComboList";
        case ControlTypes.radioButton:     return "RadioButton";
        case ControlTypes.checkButton:     return "CheckButton";
        case ControlTypes.listBox:         return "ListBox";
        case ControlTypes.listControl:     return "ListControl";
        case ControlTypes.button:          return "Button";
        case ControlTypes.date:            return "Date";
        case ControlTypes.time:            return "Time";
        case ControlTypes.dateTime:        return "DateTime";
        case ControlTypes.password:        return "Password";
    }
}

export function stringToBorderStyle(str?: string): BorderStyles | undefined {
    switch (str?.toLowerCase()) {
        case "none":    return BorderStyles.none;
        case "solid":   return BorderStyles.solid;
        case "double":  return BorderStyles.double;
        case "groove":  return BorderStyles.groove;
        case "ridge":   return BorderStyles.ridge;
        case "inset":   return BorderStyles.inset;
        case "outset":  return BorderStyles.outset;
    }
}

export function borderStyleToString(style?: BorderStyles): string | undefined {
    switch (style) {
        case BorderStyles.none:     return "None";
        case BorderStyles.solid:    return "Solid";
        case BorderStyles.double:   return "Double";
        case BorderStyles.groove:   return "Groove";
        case BorderStyles.ridge:    return "Ridge";
        case BorderStyles.inset:    return "Inset";
        case BorderStyles.outset:   return "Outset";
    }
}

export function stringToCursorType(str?: string): CursorTypes | undefined {
    switch (str?.toLowerCase()) {
        case "auto":                  return CursorTypes.auto;
        case "crosshair":             return CursorTypes.crossHair;
        case "default":               return CursorTypes.default;
        case "pointer":               return CursorTypes.pointer;
        case "move":                  return CursorTypes.move;
        case "eresize":               return CursorTypes.eResize;
        case "neresize":              return CursorTypes.nEResize;
        case "nresize":               return CursorTypes.nResize;
        case "nwresize":              return CursorTypes.nWResize;
        case "wresize":               return CursorTypes.wResize;
        case "swresize":              return CursorTypes.sWResize;
        case "sresize":               return CursorTypes.sResize;
        case "seresize":              return CursorTypes.sEResize;
        case "text":                  return CursorTypes.text;
        case "wait":                  return CursorTypes.wait;
        case "help":                  return CursorTypes.help;
    }
}

export function cursorTypeToString(type?: CursorTypes): string | undefined {
    switch (type) {
        case CursorTypes.auto:         return "Auto";
        case CursorTypes.crossHair:    return "CrossHair";
        case CursorTypes.default:      return "Default";
        case CursorTypes.pointer:      return "Pointer";
        case CursorTypes.move:         return "Move";
        case CursorTypes.eResize:      return "EResize";
        case CursorTypes.nEResize:     return "NEResize";
        case CursorTypes.nResize:      return "NResize";
        case CursorTypes.nWResize:     return "NWResize";
        case CursorTypes.wResize:      return "WResize";
        case CursorTypes.sWResize:     return "SWResize";
        case CursorTypes.sResize:      return "SResize";
        case CursorTypes.sEResize:     return "SEResize";
        case CursorTypes.text:         return "Text";
        case CursorTypes.wait:         return "Wait";
        case CursorTypes.help:         return "Help";
    }
}

export function stringToElementAlign(str?: string): ElementAlignments | undefined {
    switch (str?.toLowerCase()) {
        case "default":   return ElementAlignments.default;
        case "right":     return ElementAlignments.right;
        case "newline":   return ElementAlignments.newLine;
    }
}

export function elementAlignToString(align?: ElementAlignments): string | undefined {
    switch (align) {
        case ElementAlignments.default:   return "Default";
        case ElementAlignments.right:     return "Right";
        case ElementAlignments.newLine:   return "NewLine";
    }
}

export function stringToVerticalAlign(str?: string): VerticalAlignments | undefined {
    switch (str?.toLowerCase()) {
        case "default":        return VerticalAlignments.default;
        case "baseline":       return VerticalAlignments.baseline;
        case "middle":         return VerticalAlignments.middle;
        case "sub":            return VerticalAlignments.sub;
        case "super":          return VerticalAlignments.super;
        case "texttop":        return VerticalAlignments.textTop;
        case "textbottom":     return VerticalAlignments.textBottom;
        case "top":            return VerticalAlignments.top;
        case "bottom":         return VerticalAlignments.bottom;
    }
}

export function verticalAlignToString(align?: VerticalAlignments): string | undefined {
    switch (align) {
        case VerticalAlignments.default:     return "Default";
        case VerticalAlignments.baseline:    return "Baseline";
        case VerticalAlignments.middle:      return "Middle";
        case VerticalAlignments.sub:         return "Sub";
        case VerticalAlignments.super:       return "Super";
        case VerticalAlignments.textTop:     return "TextTop";
        case VerticalAlignments.textBottom:  return "TextBottom";
        case VerticalAlignments.top:         return "Top";
        case VerticalAlignments.bottom:      return "Bottom";
    }
}

export function stringToAlignment(str?: string): Alignments | undefined {
    switch (str?.toLowerCase()) {
        case "default":     return Alignments.default;
        case "left":        return Alignments.left;
        case "center":      return Alignments.center;
        case "right":       return Alignments.right;
        case "justify":     return Alignments.justify;
    }
}

export function alignmentToString(align?: Alignments): string | undefined {
    switch (align) {
        case Alignments.default:     return "Default";
        case Alignments.left:        return "Left";
        case Alignments.center:      return "Center";
        case Alignments.right:       return "Right";
        case Alignments.justify:     return "Justify";
    }
}

export function stringToImagePosition(str: string): ImagePositions | undefined {
    switch (str.toLowerCase()) {
        case "left":      return ImagePositions.left;
        case "right":     return ImagePositions.right;
        case "top":       return ImagePositions.top;
        case "bottom":    return ImagePositions.bottom;
        case "imageonly": return ImagePositions.imageOnly;
        case "none":      return ImagePositions.none;
    }
}

export function imagePositionToString(type: ImagePositions): string {
    switch (type) {
        case ImagePositions.left:      return "Left";
        case ImagePositions.right:     return "Right";
        case ImagePositions.top:       return "Top";
        case ImagePositions.bottom:    return "Bottom";
        case ImagePositions.imageOnly: return "ImageOnly";
        case ImagePositions.none:      return "None";
    }
}

export function stringToOrientation(str?: string): Orientations | undefined {
    switch (str?.toLowerCase()) {
        case "default":    return Orientations.default;
        case "row":        return Orientations.row;
        case "column":     return Orientations.column;
    }
}

export function orientationToString(type?: Orientations): string | undefined {
    switch (type) {
        case Orientations.default:  return "Default";
        case Orientations.row:      return "Row";
        case Orientations.column:   return "Column";
    }
}

export function elementTypeConstantsToTypeName(type: ElementTypes): MDMElementTypeName {
    switch (type) {
        case ElementTypes.category:               return "Category";
        case ElementTypes.analysisSubheading:     return "AnalysisSubheading";
        case ElementTypes.analysisBase:           return "AnalysisBase";
        case ElementTypes.analysisSubtotal:       return "AnalysisSubtotal";
        case ElementTypes.analysisSummaryData:    return "AnalysisSummaryData";
        case ElementTypes.analysisDerived:        return "AnalysisDerived";
        case ElementTypes.analysisTotal:          return "AnalysisTotal";
        case ElementTypes.analysisMean:           return "AnalysisMean";
        case ElementTypes.analysisStdDev:         return "AnalysisStdDev";
        case ElementTypes.analysisStdErr:         return "AnalysisStdErr";
        case ElementTypes.analysisSampleVariance: return "AnalysisSampleVariance";
        case ElementTypes.analysisMinimum:        return "AnalysisMinimum";
        case ElementTypes.analysisMaximum:        return "AnalysisMaximum";
        case ElementTypes.analysisCategory:       return "AnalysisCategory";
        case ElementTypes.categoryList:           return "CategoryList";
        case ElementTypes.arrayLevel:             return "ArrayLevel";
        case ElementTypes.arrayIndex:             return "ArrayIndex";
        case ElementTypes.arrayCategory:          return "ArrayCategory";
    }
}

export function displayOrderToScript(order: DisplayOrders): string {
    switch (order) {
        case DisplayOrders.normal:      return "";
        case DisplayOrders.randomize:   return "ran";
        case DisplayOrders.rotate:      return "rot";
        case DisplayOrders.reverse:     return "rev";
        case DisplayOrders.ascending:   return "asc";
        case DisplayOrders.descending:  return "desc";
    }
}

export function displayOrderToXmlValue(order: DisplayOrders): string {
    switch (order) {
        case DisplayOrders.normal:       return "";
        case DisplayOrders.randomize:    return "Randomized";
        case DisplayOrders.rotate:       return "Rotated";
        case DisplayOrders.reverse:      return "Reversed";
        case DisplayOrders.ascending:    return "Ascending";
        case DisplayOrders.descending:   return "Descending";
    }
}

export function stringToDisplayOrder(str?: string): DisplayOrders {
    switch (str?.toLowerCase()) {
        case "randomize":      return DisplayOrders.randomize;
        case "rotate":         return DisplayOrders.rotate;
        case "reverse":        return DisplayOrders.reverse;
        case "ascending":      return DisplayOrders.ascending;
        case "descending":     return DisplayOrders.descending;
        default:               return DisplayOrders.normal;
    }
}

export function variableUsageFlagsToString(usage: VariableUsageFlags): string {
    const flags: string[] = [];
    if (bitAnd(usage, VariableUsageFlags.grid)) {
        flags.push("Grid");
    }
    if (bitAnd(usage, VariableUsageFlags.compound)) {
        flags.push("Compound");
    }
    if (bitAnd(usage, VariableUsageFlags.class)) {
        flags.push("Class");
    }
    if (bitAnd(usage, VariableUsageFlags.array)) {
        flags.push("Array");
    }
    // HelperField 包含 SourceFile / Coding / OtherSpecify / Multiplier 四个子类
    // + 如果同时指定了HelperField和子类，出示子类并忽略HelperField
    // + 如果指定了HelperField同时未指定子类，出示HelperField
    if (bitAnd(usage, VariableUsageFlags.helperField)) {
        const excludeHelperField = usage & (~VariableUsageFlags.helperField);
        // 无子类
        if (!bitAnd(excludeHelperField, VariableUsageFlags.sourceFile | VariableUsageFlags.coding | VariableUsageFlags.otherSpecify | VariableUsageFlags.multiplier)) {
            flags.push("HelperField");
        }
        else {
            if (bitAnd(excludeHelperField, VariableUsageFlags.sourceFile)) {
                flags.push("SourceFile");
            }
            if (bitAnd(excludeHelperField, VariableUsageFlags.coding)) {
                flags.push("Coding");
            }
            if (bitAnd(excludeHelperField, VariableUsageFlags.otherSpecify)) {
                flags.push("OtherSpecify");
            }
            if (bitAnd(excludeHelperField, VariableUsageFlags.multiplier)) {
                flags.push("Multiplier");
            }
        }
    }
    if (bitAnd(usage, VariableUsageFlags.filter)) {
        flags.push("Filter");
    }
    if (bitAnd(usage, VariableUsageFlags.weight)) {
        flags.push("Weight");
    }
    return flags.length > 0 ? flags.join(", ") : "";
}

export function dataTypeToString(dataType?: DataTypes): string {
    switch (dataType) {
        case DataTypes.long:             return "long";
        case DataTypes.text:             return "text";
        case DataTypes.categorical:      return "categorical";
        case DataTypes.date:             return "date";
        case DataTypes.double:           return "double";
        case DataTypes.boolean:          return "boolean";
        default:                         return "";
    }
}

export function stringToDataType(str?: string): DataTypes | undefined {
    switch (str?.toLowerCase()) {
        case "long":         return DataTypes.long;
        case "text":         return DataTypes.text;
        case "categorical":  return DataTypes.categorical;
        case "date":         return DataTypes.date;
        case "double":       return DataTypes.double;
        case "boolean":      return DataTypes.boolean;
    }
}

export function infoTypeToString(infoType: InfoTypes): string {
    switch (infoType) {
        case InfoTypes.values:  return "values";
        case InfoTypes.missing: return "missing";
        case InfoTypes.flags:   return "flags";
        case InfoTypes.other:   return "other";
    }
}

export function stringToInfoType(str?: string): InfoTypes | undefined {
    switch (str?.toLowerCase()) {
        case "values":    return InfoTypes.values;
        case "missing":   return InfoTypes.missing;
        case "flags":     return InfoTypes.flags;
        case "other":     return InfoTypes.other;
    }
}

export function mdmValueToXmlType(value: number | string | boolean, dataType: DataTypes): string {
    if (typeof value === "string") {
        return dataType === DataTypes.date ? "7" : "8";
    }
    else if (typeof value === "boolean") {
        return "11";
    }
    else {
        return Number.isInteger(value) && dataType === DataTypes.long ? "3" : "5";
    }
}

export function xmlValueToMdmValue(str?: string, type?: string): string | number | boolean | undefined {
    return type === "11" ? stringToBoolean(str)
        : type === "3" ? stringToInteger(str)
        : type === "5" ? stringToFloat(str)
        : str;
}

export function xmlValueToMdmValueNoBoolean(str?: string, type?: string): string | number | undefined {
    return type === "3" ? stringToInteger(str) : type === "5" ? stringToFloat(str) : str;
}

export function metadataFieldFlagToDataType(flag: MDMFieldFlags): DataTypes {
    switch (flag) {
        case MDMFieldFlags.long:        return DataTypes.long;
        case MDMFieldFlags.text:        return DataTypes.text;
        case MDMFieldFlags.categorical: return DataTypes.categorical;
        case MDMFieldFlags.date:        return DataTypes.date;
        case MDMFieldFlags.double:      return DataTypes.double;
        case MDMFieldFlags.boolean:     return DataTypes.boolean;
        default:                            return DataTypes.none;
    }
}

export function metadataUsageTypeToVariableUsageType(type: MDMUsageTypeFlags): VariableUsageFlags {
    let result = VariableUsageFlags.variable;
    if (bitAnd(type, MDMUsageTypeFlags.filter)) {
        result |= VariableUsageFlags.filter;
    }
    if (bitAnd(type, MDMUsageTypeFlags.weight)) {
        result |= VariableUsageFlags.weight;
    }
    if (bitAnd(type, MDMUsageTypeFlags.multiplier)) {
        result |= VariableUsageFlags.multiplier;
    }
    if (bitAnd(type, MDMUsageTypeFlags.coding)) {
        result |= VariableUsageFlags.coding;
    }
    if (bitAnd(type, MDMUsageTypeFlags.sourceFile)) {
        result |= VariableUsageFlags.sourceFile;
    }
    if (bitAnd(type, MDMUsageTypeFlags.otherSpecify)) {
        result |= VariableUsageFlags.otherSpecify;
    }
    if (bitAnd(type, MDMUsageTypeFlags.helperField)) {
        result |= VariableUsageFlags.helperField;
    }
    return result;
}

export function metadataCategoryElementTypeToElementType(type: MDMCategoryElementTypeFlags): ElementTypes {
    switch (type) {
        case MDMCategoryElementTypeFlags.analysisBase:             return ElementTypes.analysisBase;
        case MDMCategoryElementTypeFlags.analysisCategory:         return ElementTypes.analysisCategory;
        case MDMCategoryElementTypeFlags.analysisMaximum:          return ElementTypes.analysisMaximum;
        case MDMCategoryElementTypeFlags.analysisMean:             return ElementTypes.analysisMean;
        case MDMCategoryElementTypeFlags.analysisMinimum:          return ElementTypes.analysisMinimum;
        case MDMCategoryElementTypeFlags.analysisSampleVariance:   return ElementTypes.analysisSampleVariance;
        case MDMCategoryElementTypeFlags.analysisStdDev:           return ElementTypes.analysisStdDev;
        case MDMCategoryElementTypeFlags.analysisStdErr:           return ElementTypes.analysisStdErr;
        case MDMCategoryElementTypeFlags.analysisSubHeading:       return ElementTypes.analysisSubheading;
        case MDMCategoryElementTypeFlags.analysisSubTotal:         return ElementTypes.analysisSubtotal;
        case MDMCategoryElementTypeFlags.analysisSummaryData:      return ElementTypes.analysisSummaryData;
        case MDMCategoryElementTypeFlags.analysisTotal:            return ElementTypes.analysisTotal;
    }
}

export function metadataSortOrderToDisplayOrders(order: MDMSortOrder): DisplayOrders {
    if (order.rotate) {
        return DisplayOrders.rotate;
    }
    if (order.randomize) {
        return DisplayOrders.randomize;
    }
    if (order.reverse) {
        return DisplayOrders.reverse;
    }
    if (order.ascending) {
        return DisplayOrders.ascending;
    }
    if (order.descending) {
        return DisplayOrders.descending;
    }
    return DisplayOrders.normal;
}

export function metadataUserContextFlagToString(flag: MDMUserContextFlags): string {
    switch (flag) {
        case MDMUserContextFlags.analysis:     return "ANALYSIS";
        case MDMUserContextFlags.cardcol:      return "CARDCOL";
        case MDMUserContextFlags.cati:         return "CATI";
        case MDMUserContextFlags.mobile:       return "MOBILE";
        case MDMUserContextFlags.paper:        return "PAPER";
        case MDMUserContextFlags.qc:           return "QC";
        case MDMUserContextFlags.question:     return "QUESTION";
        case MDMUserContextFlags.sav:          return "SAV";
        case MDMUserContextFlags.web:          return "WEB";
        case MDMUserContextFlags.webapp:       return "WEBAPP";
        case MDMUserContextFlags.papi:         return "PAPI";
    }
}

export function metadataLabelTypeFlagToString(flag: MDMLabelTypeFlags): string {
    switch (flag) {
        case MDMLabelTypeFlags.instruction:      return "INSTRUCTION";
        case MDMLabelTypeFlags.label:            return "LABEL";
        case MDMLabelTypeFlags.shortName:        return "SHORTNAME";
    }
}

// test

export function isMDMDocument(obj?: IMDMObject): obj is MDMDocument {
    return obj !== undefined && hasProperty(obj, "objectFlag") && obj["objectFlag"] === ObjectFlags.document;
}

export function isMDMElement(obj?: any): obj is MDMElement {
    return obj !== undefined && hasProperty(obj, "objectFlag") && obj["objectFlag"] === ObjectFlags.element;
}

export function isMDMElements(obj?: any): obj is MDMElements {
    return obj !== undefined && hasProperty(obj, "objectFlag") && obj["objectFlag"] === ObjectFlags.elements;
}

export function isMDMVariable(obj?: any): obj is Variable {
    return obj !== undefined && hasProperty(obj, "objectFlag") && obj["objectFlag"] === ObjectFlags.variable;
}

export function isMDMField(obj?: any): obj is MDMField {
    return obj !== undefined && hasProperty(obj, "objectFlag") && obj["objectFlag"] === ObjectFlags.field;
}

export function isMDMClass(obj?: any): obj is Class {
    return obj !== undefined && hasProperty(obj, "objectFlag") && obj["objectFlag"] === ObjectFlags.class;
}

export function isMDMCompound(obj?: any): obj is Compound {
    return obj !== undefined && hasProperty(obj, "objectFlag") && obj["objectFlag"] === ObjectFlags.compound;
}

export function isMDMPage(obj?: any): obj is Page {
    return obj !== undefined && hasProperty(obj, "objectFlag") && obj["objectFlag"] === ObjectFlags.page;
}

export function isMDMList<T>(obj?: any): obj is MDMListOf<T> {
    return obj !== undefined && Array.isArray(obj) && hasProperty(obj, "deleted");
}

export function isMDMElementInstance(obj?: any): obj is MDMElementInstance {
    return obj !== undefined && hasProperty(obj, "objectFlag") && obj["objectFlag"] === ObjectFlags.elementInstance;
}

// XML
export function isElement(obj?: any): obj is Element {
    return obj && "nodeType" in obj && obj["nodeType"] === 1;
}
